✨ fix(reviewer-lane): true creation-time ordering + structured re-escalation hand-off (#5617 items 3–4) - #5682
Merged
kubestellar-prow[bot] merged 3 commits intoSep 2, 2026
Conversation
…PR number Deferred item 4 of hivecommons#5617. The reviewer lane's work list is capped at reviewerMaxPRsPerKick rows and documented "oldest first", but the rows carried no age signal at all: ci-failing.json recorded no creation time, so formatReviewerWorkList ordered by (repo name, PR number). PR numbers are monotonic only WITHIN a repo, so that proxy sorted by repo NAME first. Against the per-kick cap that is a starvation bug, not a cosmetic one — a month-old escalated PR in "zeta/service" sits behind three newer ones in "alpha/console" on every kick, forever, and the reviewer never reaches it. github.PullRequest already carried CreatedAt from enumeration; it was simply never threaded into the ci-failing rows. writeMergeEligible now records it and formatReviewerWorkList orders on it, rendering the key as an `opened:` line so the reviewer can verify the ordering without `gh pr list` (which the kick's INVARIANTS forbid). Rows with no creation time — a ci-failing.json written by an older hub, or a forge that omitted the field — sort LAST and keep the old (repo, number) proxy among themselves: an unproven age must not jump ahead of a measured one. That also makes the change a no-op on a stale artifact rather than a reshuffle. Refs hivecommons#5617 (item 4 of 4 — does not close the tracker) Signed-off-by: Doug Baggett <doug.baggett@gmail.com>
…fter a reviewer pass Deferred item 3 of hivecommons#5617. The reviewer lane is a one-pass ladder: a PR carrying `reviewer-passed` is excluded from the reviewer work list permanently, so when it goes red again the escalation comment is the LAST thing the machinery will ever say about it. That comment was the generic first-escalation body — nothing but the label set distinguished a terminal hand-off from a first escalation, and nothing told the human that a mechanical repair had already been tried and had not held. The hub never observes a reviewer verdict directly (it is a label edit, usually a direct `gh pr edit`); the only moment it can see one is Sweep's reviewer-verdict reconciliation (hivecommons#5511, gap G1). So record it there: - Entry gains ReviewerPassedSHA/ReviewerPassedAt, stamped by the reconciliation and deliberately SURVIVING the ledger reset that same reconciliation performs. Keyed on the SHA so a repeat reconciliation cannot walk the timestamp forward and misdate the hand-off. Cleared only with the entry itself — a PR that goes green has converged and starts a fresh story. - Store.ReviewerPass exposes it; runEscalationSweep uses it to choose between CommentBody and the new HandoffCommentBody. The note states the head commit the reviewer left on the branch, when the PR was returned to the automated lane, that the attempt count is measured FROM that verdict (the ledger restarts there), and that no further automated pass is coming. It points at the reviewer's own audited record — the `Reviewer adjudication:` relay comment attributed as `agent_pr_reviewed`, plus the advisory bead — rather than restating it: the hub never saw the reviewer's reasoning, so summarising it would be invention. The ntfy page is retitled on this path too; a hand-off with nothing automated behind it is materially more urgent than a first escalation. Refs hivecommons#5617 (item 3 of 4 — does not close the tracker) Signed-off-by: Doug Baggett <doug.baggett@gmail.com>
…ow-ups Both changes are user-visible: the escalated-PR work list changes order, and the second-escalation PR comment a human reads changes content. Refs hivecommons#5617 Signed-off-by: Doug Baggett <doug.baggett@gmail.com>
clubanderson
reviewed
Sep 2, 2026
clubanderson
left a comment
Member
There was a problem hiding this comment.
Review — OK to merge.
Verified against the v5 base (correct branch for the lane) and the #5617 tracker:
- Item 4 (creation-time ordering):
CreatedAtis threaded end-to-end —writeMergeEligiblerecordspr.CreatedAtinto theci-failing.jsonrow, andformatReviewerWorkListsorts on it. The comparator is a valid strict weak order: known-age rows first by time, ties and unknown-age rows fall back to the old(repo, number)proxy, so a stale artifact is a no-op rather than a reshuffle. The renderedopened:line makes the key auditable from inside the kick. Both ordering tests encode the actual starvation scenario (late-alphabet repo, oldest PR) and fail under the old key. - Item 3 (hand-off note): the stamp is taken at the only observable moment (Sweep's #5511 G1 reviewer-verdict reconciliation), survives the ledger reset it accompanies, is SHA-keyed so a repeat reconciliation cannot walk the timestamp, and is cleared only with the entry (green = fresh story, as documented).
runEscalationSweeproutes toHandoffCommentBodysolely onReviewerPassok; first escalations are byte-identical. The body's claims match the verified ledger semantics: attempts measured from the verdict (test pinsAttempts == 1after reconciliation) and permanent reviewer-passed exclusion from the work list (one-pass invariant). - Formal model:
formal-verify (escalation)ran and passed on the head SHA (it's beyond the first check-runs page — confirmed via the paginated API). Sweep's transitions are untouched; only fields were added, so no model drift. - No double-audit with #5588: the diff emits no new audit events — the hand-off note points at the existing
Reviewer adjudication:relay comment / advisory bead rather than re-recording them. Only the ntfy title changes, and only on the hand-off path. - Partial-record rendering (no empty backticks, no
0001-01-01) is tested; the #5670 overlap inrunEscalationSweepis disjoint as described.
Minor, non-blocking: CreatedAt has no omitempty, so a forge-omitted time serializes as the zero time — harmless, since the reader treats zero as unknown and sorts it last.
Contributor
|
[APPROVALNOTIFIER] This PR is APPROVED Approval requirements bypassed by manually added approval. This pull-request has been approved by: The full list of commands accepted by this bot can be found here. The pull request process is described here DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
This was referenced Sep 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two of the four follow-ups deferred from the #5480 reviewer-lane delivery. Both are small, independent, and self-contained; neither changes who is allowed to do what.
Item 1 (G2 pending-wipe) shipped in #5634. Item 2 (the
reviewer-lane.mdkick template) is not claimed here — see below.Fix
Item 4 — true creation-time ordering
formatReviewerWorkListordered on(repo, number)becauseci-failing.jsoncarried no creation time. PR numbers are monotonic only within a repo, so that key sorted by repo name first. Combined with thereviewerMaxPRsPerKickcap this is a starvation bug, not a cosmetic one: a month-old escalated PR inzeta/servicesits behind three newer ones inalpha/consoleon every kick, forever, and the reviewer never reaches it.github.PullRequest.CreatedAtwas already populated at enumeration and simply never threaded through.writeMergeEligiblenow records it; the work list orders on it and renders it as anopened:line, so the reviewer can verify the ordering itself — the kick's INVARIANTS forbidgh pr list.Rows with no creation time (a
ci-failing.jsonwritten by an older hub, or a forge that omitted the field) sort last and keep the old proxy among themselves. An unproven age must not jump ahead of a measured one, and this makes the change a no-op on a stale artifact rather than a reshuffle.Item 3 — structured hand-off note
The hub never observes a reviewer verdict directly: it is a label edit, usually a direct
gh pr edit. The single moment it can see one isSweep's reviewer-verdict reconciliation (#5511, gap G1) — so the record is taken there.EntrygainsReviewerPassedSHA/ReviewerPassedAt, stamped by that reconciliation and deliberately surviving the ledger reset the same reconciliation performs. Keyed on the SHA, so a repeat reconciliation cannot walk the timestamp forward and misdate the hand-off. Cleared only with the entry itself — a PR that goes green has converged and starts a fresh story.Store.ReviewerPassexposes it;runEscalationSweepuses it to pickHandoffCommentBodyoverCommentBody.The note gives the head commit the reviewer left, when the PR returned to the automated lane, that the attempt count is measured from that verdict (the ledger restarts there), and that no further automated pass is coming. It points at the reviewer's own audited record — the
Reviewer adjudication:relay comment attributedagent_pr_reviewed, plus the advisory bead — rather than restating it: the hub never saw the reviewer's reasoning, so summarising it would be invention. The ntfy page is retitled on this path only.First escalations are byte-for-byte unchanged, and a partial record (SHA or timestamp missing) omits the field rather than rendering an empty backtick pair or
0001-01-01.Cluster claimed (files/functions)
src/pkg/scheduler/reviewer_lane.go—formatReviewerWorkListordering key +opened:row.src/cmd/hive/main.go—writeMergeEligible'sfailingPR.CreatedAt;runEscalationSweep's comment/notify selection.src/pkg/escalation/escalation.go—Entryreviewer-pass fields,Sweepstamping,Store.ReviewerPass,ReviewerHandoff+HandoffCommentBody(CommentBodykeeps its signature and behavior).src/docs/adr/0010-escalation-circuit-breaker.md,CHANGELOG.md.pkg/escalation), 2 ordering tests (pkg/scheduler), 1 end-to-end sweep test (cmd/hive).Disjoint from the open hold-gated PRs. The nearest neighbour is #5670, which also edits
runEscalationSweep— it adds alifecycleRecorderparameter and arecordBlockedcall betweenMarkEscalatedandlogger.Info. This PR touches the comment construction above that and the notifier block below it, and leaveslogger.Infoalone specifically so the two do not collide. Whichever lands second should merge cleanly; if not, the resolution is to keep both.Validation
go build ./...,go vet ./cmd/hive/ ./pkg/escalation/ ./pkg/scheduler/, andgofmt -lclean on every touched file.go test ./cmd/hive/ ./pkg/escalation/ ./pkg/scheduler/passes.Sweep's state machine and transitions are untouched, sosrc/formal/escalationis unaffected.Not claimed
Item 2 (embedded
reviewer-lane.mdkick template) is deliberately left for a separate PR. It is a change to the kick resolution chain rather than to the lane's data, and the rendered template has to reproduce the ~25 safety strings pinned byTestBuildReviewerMessage_ContractAtL5— including the hard ACMM dormancy gate, which must stay in Go so an operator-editable template cannot switch the lane on below L5. That deserves its own review, not a rider on this one.Related issues
Refs #5617 (items 3 and 4 of 4 — does not close the tracker)
Testing
cd src && go build ./...cd src && go test ./...— ran the three affected packages (./cmd/hive/,./pkg/escalation/,./pkg/scheduler/); all pass.— hive: backend=claude model=claude-opus-5